虽然大型项目会使用cmake,手写makefile的情况比较少,但是基本语法还是要熟悉的。
简介 make 和 makefile 是 Linux 系统下 C/C++ 工程的编译工具,它们用来自动化配置实验环境,编译项目文件。makefile 文件描述了项目文件之间的依赖关系和编译规则 ,make 命令根据 makefile 文件的内容执行编译任务123。相对于 cmake 等其他的编译工具,make 和 makefile 有以下几个特点:
make 和 makefile 是 GNU 的标准工具,可以在多种平台上使用。
make 和 makefile 可以根据文件时间戳自动发现更新过的文件而减少编译的工作量。
make 和 makefile 可以灵活地定义变量、函数和条件判断等,实现复杂的编译逻辑
对于复杂的项目,能通过makefile文件将
实验环境Build,(提前设置相关路径参数)
wget/git 下载相关文件到对应目录
apt install
样例测试test,
整体运行/Benchmark运行run,
环境清理clean 统一起来
执行流程
make的默认目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。
选项 1 2 3 4 5 6 7 8 9 10 11 12 make -n make --dry-run make -C make -f makefile.llvm make inj svm.inj
debug 打印debug信息,makefile如何选择决策
--debug=v
输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等。
--debug=i
implicit,输出使用的隐含规则过程。
--debug=m
输出make读取makefile,更新makefile,执行makefile的信息。
基本语法
$(foreach var,list,text)
的语法来对list中的每个元素执行text,并用var来引用当前元素1。你也可以用
$(wildcard pattern)
的语法来匹配指定模式的文件,并返回其列表
$(subst from,to,text)
的语法来把text中的from替换为to
$(patsubst pattern,replacement,text)
的语法来把text中匹配pattern的部分替换为replacement
include <filenames>
,make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出;
-include <filenames>
,当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出;
addprefix的功能增加前缀,例如$(addprefix -I,./Inc)
执行后为 -I ./Inc
OUTPUT_FILES = $(addsuffix .out, $(addprefix $(OUTPUT_DIR)/, $(BENCH_MAINNAME)))
文件名处理函数 dir, notdir, suffix, basename (网站 basename例子写错了)
.PHONY: clean
是为了有clean名称的文件时,防止把make clean
理解成生成clean文件,而不是使用clean规则。
一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。所以一般第一个写all
赋值符号
1 2 3 4 = 是最基本的赋值 := 是覆盖之前的值 ?= 是如果没有被赋值过就赋予等号后面的值 += 是添加等号后面的值
makefile中命令前加一个@
的作用是不让make显示出要执行的命令
$@
is the name of the target being generated, and$<
the first prerequisite (usually a source file). You can find a list of all these special variables in the GNU Make manual.
For example, consider the following declaration:
all: library.cpp main.cpp
In this case:
$@
evaluates to all
$<
evaluates to library.cpp
$^
evaluates to library.cpp main.cpp
常见问题 1 Makefile:22: *** missing separator. Stop.
命令开头要用Tab,不是空格。别用vscode,用vim写
Makefile 隐含规则
“隐含规则”也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。
例如,把[.c]文件编译成[.o]文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的[.o]文件。
将头文件变成.gch
precompiled headers
许多的隐含规则都是使用了“后缀规则”来定义的
变量.SUFFIXES
存储了默认的依赖目标,可以修改。默认的后缀列表是:.out,.a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el
要让make知道一些特定的后缀,我们可以使用伪目标”.SUFFIXES”来定义或是删除,如:
把后缀.hack和.win加入后缀列表中的末尾。
先删除默认后缀,后定义自己的后缀列表。
1 2 .SUFFIXES: # 删除默认的后缀 .SUFFIXES: .c .o .h # 定义自己的后缀
模式规则
常见使用模式规则来定义一个隐含规则
至少在规则的目标定义中要包含”%”
1 %.o : %.c ; <command ......>
老式风格的”后缀规则”
后缀规则是一个比较老式的定义隐含规则的方法。
后缀规则会被模式规则逐步地取代。因为模式规则更强更清晰。为了和老版本的Makefile兼容,GNU make同样兼容于这些东西。
后缀规则有两种方式:”双后缀”和”单后缀”。
双后缀规则定义了一对后缀:目标文件的后缀和依赖目标(源文件)的后缀。如”.c.o”相当于”%o : %c”。
单后缀规则只定义一个后缀,也就是源文件的后缀。如”.c”相当于”% : %.c”。
1 2 3 4 5 .cpp.o: $(CPPCOMPILE) -c $(COMPILEOPTION) $(INCLUDEDIR) $< .c.o: $(CCOMPILE) -c $(COMPILEOPTION) $(INCLUDEDIR) $<
Makefile 手写技巧示例 框架 1 2 3 4 5 6 7 SIM_ROOT ?= $(shell readlink -f "$(CURDIR) ") CLEAN=$(findstring clean,$(MAKECMDGOALS) ) include common/Makefile.common$(STANDALONE) : $(LIB_CARBON) $(LIB_SIFT) $(LIB_DECODER) @$(MAKE) $(MAKE_QUIET) -C $(SIM_ROOT) /standalone
Makefile.common文件中的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 include $(SIM_ROOT) /Makefile.configDIRECTORIES := ${shell find $(SIM_ROOT) /common -type d -print} LIBCARBON_SOURCES = $(foreach dir ,$(DIRECTORIES) ,$(wildcard $(dir) /*.cc) ) \ $(wildcard $(SIM_ROOT) /common/config/*.cpp) ifeq ($(SNIPER_TARGET_ARCH) ,ia32) CXXFLAGS += -m32 -march=i686 -DTARGET_IA32 LD_FLAGS += -m32 endif ifeq ($(SNIPER_TARGET_ARCH) ,intel64) CXXFLAGS += -fPIC -DTARGET_INTEL64 LD_FLAGS += endif %.d: %.cpp $(_MSG) '[DEP ]' $(subst $(shell readlink -f $(SIM_ROOT) ) /,,$(shell readlink -f $@ ) ) $(_CMD) $(CXX) -MM -MG $(CPPFLAGS) $(CXXFLAGS) $< | sed -n "H;$$ {g;s@.*:\(.*\)@$* .o $@ : \$$\(wildcard\1\)@;p}" >$@
Makefile.config 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ARCH_QUERY=$(shell uname -m) ifeq ($(ARCH_QUERY) ,i686)SNIPER_TARGET_ARCH = ia32 else ifeq ($(ARCH_QUERY) ,x86_64)SNIPER_TARGET_ARCH ?= intel64 else $(error Unknown target arch: $(ARCH_QUERY) ) endif endif PIN_HOME ?= xxx PIN_ROOT := $(PIN_HOME) CC ?= gcc CXX ?= g++ ifneq ($(DEBUG_SHOW_COMPILE) ,) SHOW_COMPILE=1 MAKE_QUIET= _MSG=@echo >/dev/null _CMD= else SHOW_COMPILE= MAKE_QUIET=--quiet _MSG=@echo _CMD=@ endif
打印make的详细信息make DEBUG_SHOW_COMPILE=1 DEBUG=1 -j 16|tee make.log |my_hl
打印make时每项依赖文件的情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 test-score: test-all @$(MAKE) test-all | cut -d\ -f 2 | grep 'PASS\|FAIL' | sort | uniq -c PASS = \033[92mPASS\033[0m FAIL = \033[91mFAIL\033[0m test-build: all @echo " $(PASS) Build"
实现在Makefile里下载 根据是否存在文件来执行makefile语句
1 2 3 4 5 6 7 8 9 10 ROAD_URL = http://www.dis.uniroma1.it/challenge9/data/USA-road-d/USA-road-d.USA.gr.gz $(RAW_GRAPH_DIR) /USA-road-d.USA.gr.gz: wget -P $(RAW_GRAPH_DIR) $(ROAD_URL) $(RAW_GRAPH_DIR) /USA-road-d.USA.gr: $(RAW_GRAPH_DIR) /USA-road-d.USA.gr.gz cd $(RAW_GRAPH_DIR) gunzip < $< > $@ $(GRAPH_DIR) /road.sg: $(RAW_GRAPH_DIR) /USA-road-d.USA.gr converter ./converter -f $< -b $@
每个文件是单独EXE 目录下所有源文件编译成单独的可执行文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 CFLAGS = -g -O0 SRC_DIR = ../ BUILD_DIR = ./ SRCS = $(wildcard $(SRC_DIR) *.c) FILENAME = $(SRCS:.c=) PROGS = $(addprefix $(BUILD_DIR) ,$(notdir $(FILENAME) ) ) .PHONY : allall: $(PROGS) @echo "-- build all .c files" @echo Building for x86 architecture $(BUILD_DIR) %: $(SRC_DIR) %.c gcc $(CFLAGS) -o $@ $<
另一种写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 CC=mpicc CXX=mpicxx OPENMP=-fopenmp SOURCES:=$(shell find $(.) -name '*.c') SOURCESCXX:=$(shell find $(.) -name '*.cpp') LIB=-lm OBJS=$(SOURCES:%.c=%) OBJS+=$(SOURCESCXX:%.cpp=%) DEBUG=-g SRC_DIR_1 = ./Lab1 SRCS_1 = $(shell find $(.) -name '*.c') FILENAME_1 = $(SRCS_1:.c=) all : $(OBJS) @echo $(SOURCES) $(SOURCESCXX) @echo "编译完成" @echo $(OBJS) if [ ! -d "build" ]; then mkdir build; fi mv $(OBJS) build %: %.c $(CC) $(DEBUG) $(OPENMP) $< $(LIB) -o $@ %: %.cpp $(CXX) $(DEBUG) $(OPENMP) $< $(LIB) -o $@ .PHONY : clean showVariableshowVariable: @echo $(SOURCES) clean: rm -rf build
多个文件变一个可执行文件 1 2 3 4 5 6 7 8 9 10 11 12 13 MPICC = mpicc LIB = -lm C_FLAGS= -O3 -mavx2 -fopenmp $(LIB) SRC_DIR = src BUILD_DIR = build/bin SRCS = $(wildcard $(SRC_DIR) /*.c) OBJ = $(SRCS:.c=.o) pivot: ${SRCS} echo "compiling $(SRC_DIR) ${FILENAME}" $(MPICC) $^ $(C_FLAGS) -o $(BUILD_DIR) /pivot
运行结果
1 2 3 4 5 $ make pivot -C .. make: 进入目录“/home/shaojiemike/github/IPCC2022-preliminary” echo "compiling src src/Combination src/heapSort src/SunDistance src/MPI src/pivot" compiling src src/Combination src/heapSort src/SunDistance src/MPI src/pivot mpicc src/Combination.c src/heapSort.c src/SunDistance.c src/MPI.c src/pivot.c -O3 -mavx2 -fopenmp -lm -o build/bin/pivot
更复杂 保留.o文件,区分Flag与lib
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 CC = g++ MPICC = mpicc C_FLAGS= -O3 -fopenmp LIB = -lgomp INCLUDEPATH = feGRASS SRC_DIR = feGRASS BUILD_DIR = build/bin SRCS = $(wildcard $(SRC_DIR) /*.cpp) OBJ = $(SRCS:.cpp=.o) DEBUG_OBJ = $(SRCS:.cpp=_debug.o) TIME_OBJ = $(SRCS:.cpp=_time.o) FILENAME = $(SRCS:.cpp=) .DEFAULT_GOAL := all all : ${OBJ} echo "compiling $(SRC_DIR) ${FILENAME}" $(CC) $^ $(LIB) -o $(BUILD_DIR) /main debugPrint: ${DEBUG_OBJ} echo "compiling $(SRC_DIR) ${FILENAME}" $(CC) $^ $(LIB) -o $(BUILD_DIR) /main timePrint: ${TIME_OBJ} echo "compiling $(SRC_DIR) ${FILENAME}" $(CC) $^ $(LIB) -o $(BUILD_DIR) /main %.o: %.cpp $(CC) $(C_FLAGS) -c $< -o $@ %_debug.o: %.cpp $(CC) -DDEBUG -DTIME $(C_FLAGS) -c $< -o $@ %_time.o: %.cpp $(CC) -DTIME $(C_FLAGS) -c $< -o $@ checkdirs: $(BUILD_DIR) $(BUILD_DIR) : @mkdir -p $@ .PHONY : cleanclean: rm -rf $(BUILD_DIR) /main ${SRC_DIR}/*.o
将obj与src分离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 CC = g++ MPICC = mpicc C_FLAGS= -O3 -fopenmp LIB = -lgomp INCLUDEPATH = feGRASS SRC_DIR = feGRASS BUILD_DIR = build/bin OBJ_DIR = build/obj SRCS = $(wildcard $(SRC_DIR) /*.cpp) TMP = $(patsubst %.cpp,${OBJ_DIR}/%.cpp,$(notdir ${SRCS}) ) HEADERS = $(wildcard $(SRC_DIR) /*.h) OBJ = $(TMP:.cpp=.o) DEBUG_OBJ = $(TMP:.cpp=_debug.o) TIME_OBJ = $(TMP:.cpp=_time.o) FILENAME = $(TMP:.cpp=) $(info SRCS is: $(SRCS) ) $(info OBJ is: $(OBJ) ) $(info HEADERS is: $(HEADERS) ) .DEFAULT_GOAL := main main : $(OBJ) $(CC) $(OBJ) $(LIB) -o $(BUILD_DIR) /main debugPrint: $(DEBUG_OBJ) $(CC) $(DEBUG_OBJ) $(LIB) -o $(BUILD_DIR) /main timePrint: $(TIME_OBJ) $(CC) $(TIME_OBJ) $(LIB) -o $(BUILD_DIR) /main mpi: $(SRCS) $(MPICC) $^ $(C_FLAGS) -o $(BUILD_DIR) /main debugMpi: $(SRCS) $(MPICC) -DDEBUG -DTIME $^ $(C_FLAGS) -o $(BUILD_DIR) /main timeMpi: $(SRCS) $(MPICC) -DTIME $^ $(C_FLAGS) -o $(BUILD_DIR) /main ${OBJ_DIR}/%.o: ${SRC_DIR}/%.cpp $(HEADERS) $(CC) $(C_FLAGS) -c $< -o $@ ${OBJ_DIR}/%_debug.o: ${SRC_DIR}/%.cpp $(HEADERS) $(CC) -DDEBUG -DTIME $(C_FLAGS) -c $< -o $@ ${OBJ_DIR}/%_time.o: ${SRC_DIR}/%.cpp $(HEADERS) $(CC) -DTIME $(C_FLAGS) -c $< -o $@ checkdirs: $(BUILD_DIR) $(BUILD_DIR) : @mkdir -p $(BUILD_DIR) @mkdir -p $(OBJ_DIR) .PHONY : cleanclean: rm -rf $(BUILD_DIR) /main $(OBJ_DIR) /*.o
复杂项目的例子
分析复杂的Makefile
hl_lines 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 STANDALONE=$(SIM_ROOT) /lib/sniper $(STANDALONE) : $(LIB_CARBON) $(LIB_SIFT) $(LIB_DECODER) @$(MAKE) $(MAKE_QUIET) -C $(SIM_ROOT) /standalone LD_LIBS += -lcarbon_sim -lpthread -ldw SOURCES = $(shell ls $(SIM_ROOT) /standalone/*.cc) OBJECTS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES) ) ) TARGET = $(SIM_ROOT) /lib/sniper $(TARGET) : $(SIM_ROOT) /lib/libcarbon_sim.a $(SIM_ROOT) /sift/libsift.a $(SIM_ROOT) /decoder_lib/libdecoder.a$(TARGET) : $(OBJECTS) $(_MSG) '[LD ]' $(subst $(shell readlink -f $(SIM_ROOT) ) /,,$(shell readlink -f $@ ) ) $(_CMD) $(CXX) $(LD_FLAGS) -o $@ $(OBJECTS) $(LD_LIBS) $(OPT_CFLAGS) -std=c++0x
解释
第一个规则:$(TARGET): $(SIM_ROOT)/lib/libcarbon_sim.a $(SIM_ROOT)/sift/libsift.a $(SIM_ROOT)/decoder_lib/libdecoder.a
这行定义了 $(TARGET)(即 $(SIM_ROOT)/lib/sniper)依赖于三个库文件。这意味着在构建 $(TARGET) 之前,必须首先存在或构建这些库文件。
第二个规则:$(TARGET): $(OBJECTS)
这行则说明 $(TARGET) 还依赖于由 $(OBJECTS) 定义的一系列对象文件。这是实际编译生成可执行文件所需要的对象文件。
参考文献
https://blog.csdn.net/ET_Endeavoring/article/details/98989066